/*
 * Copyright 2002-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package wang.lcs.pay.util.print;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * Result handler that prints {@link MvcResult} details to a given output stream &mdash; for example: {@code System.out}, {@code System.err}, a custom {@code java.io.PrintWriter},
 * etc.
 *
 * <p>
 * An instance of this class is typically accessed via one of the {@link MockMvcResultHandlers#print print} or {@link MockMvcResultHandlers#log log} methods in
 * {@link MockMvcResultHandlers}.
 *
 * @author Rossen Stoyanchev
 * @author Sam Brannen
 * @since 3.2
 */
public class PrintingHttpResult {

    private static final Logger LOG = LoggerFactory.getLogger(PrintingHttpResult.class);

    private final ResultValuePrinter printer;

    /**
     * Protected constructor.
     * 
     * @param printer
     *            a {@link ResultValuePrinter} to do the actual writing
     */
    public PrintingHttpResult(ResultValuePrinter printer) {
        this.printer = printer;
    }

    /**
     * @return the result value printer
     */
    protected ResultValuePrinter getPrinter() {
        return this.printer;
    }

    /**
     * Print {@link MvcResult} details.
     * 
     * @return
     */
    public void print(HttpServletRequest request, HttpServletResponse resp) {
        try {
            this.print(new HttpResult(request, resp));
        } catch (Exception e) {
            LOG.warn("Print Error", e);
        }
    }

    public final void print(HttpResult result) throws Exception {
        this.printer.printHeading("HttpServletRequest");
        printRequest(result.getRequest());

        this.printer.printHeading("Async");
        printAsyncResult(result);

        this.printer.printHeading("HttpServletResponse");
        printResponse(result.getResponse());

        this.printer.close();
    }

    /**
     * Print the request.
     */
    protected void printRequest(HttpServletRequest request) throws Exception {
        this.printer.printValue("HTTP Method", request.getMethod());
        this.printer.printValue("Request URI", request.getRequestURI());
        this.printer.printValue("Parameters", getParamsMultiValueMap(request));
        this.printer.printValue("Headers", getRequestHeaders(request));
        printCookies(request.getCookies());
    }

    protected final HttpHeaders getRequestHeaders(HttpServletRequest request) {
        HttpHeaders headers = new HttpHeaders();
        Enumeration<?> names = request.getHeaderNames();
        while (names.hasMoreElements()) {
            String name = (String) names.nextElement();
            Enumeration<String> values = request.getHeaders(name);
            while (values.hasMoreElements()) {
                headers.add(name, values.nextElement());
            }
        }
        return headers;
    }

    protected final MultiValueMap<String, String> getParamsMultiValueMap(HttpServletRequest request) {
        Map<String, String[]> params = request.getParameterMap();
        MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<String, String>();
        for (String name : params.keySet()) {
            if (params.get(name) != null) {
                for (String value : params.get(name)) {
                    multiValueMap.add(name, value);
                }
            }
        }
        return multiValueMap;
    }

    protected void printAsyncResult(HttpResult result) throws Exception {
        HttpServletRequest request = result.getRequest();
        this.printer.printValue("Async started", request.isAsyncStarted());
        Object asyncResult = null;
        try {
            asyncResult = result.getAsyncResult(0);
        } catch (IllegalStateException ex) {
            // Not set
        }
        this.printer.printValue("Async result", asyncResult);
    }

    /**
     * Print the handler.
     */
    protected void printHandler(Object handler, HandlerInterceptor[] interceptors) throws Exception {
        if (handler == null) {
            this.printer.printValue("Type", null);
        } else {
            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                this.printer.printValue("Type", handlerMethod.getBeanType().getName());
                this.printer.printValue("Method", handlerMethod);
            } else {
                this.printer.printValue("Type", handler.getClass().getName());
            }
        }
    }

    /**
     * Print exceptions resolved through a HandlerExceptionResolver.
     */
    protected void printResolvedException(Exception resolvedException) throws Exception {
        if (resolvedException == null) {
            this.printer.printValue("Type", null);
        } else {
            this.printer.printValue("Type", resolvedException.getClass().getName());
        }
    }

    /**
     * Print the ModelAndView.
     */
    protected void printModelAndView(ModelAndView mav) throws Exception {
        this.printer.printValue("View name", (mav != null) ? mav.getViewName() : null);
        this.printer.printValue("View", (mav != null) ? mav.getView() : null);
        if (mav == null || mav.getModel().size() == 0) {
            this.printer.printValue("Model", null);
        } else {
            for (String name : mav.getModel().keySet()) {
                if (!name.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
                    Object value = mav.getModel().get(name);
                    this.printer.printValue("Attribute", name);
                    this.printer.printValue("value", value);
                    Errors errors = (Errors) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + name);
                    if (errors != null) {
                        this.printer.printValue("errors", errors.getAllErrors());
                    }
                }
            }
        }
    }

    /**
     * Print "output" flash attributes.
     */
    protected void printFlashMap(FlashMap flashMap) throws Exception {
        if (ObjectUtils.isEmpty(flashMap)) {
            this.printer.printValue("Attributes", null);
        } else {
            for (String name : flashMap.keySet()) {
                this.printer.printValue("Attribute", name);
                this.printer.printValue("value", flashMap.get(name));
            }
        }
    }

    /**
     * Print the response.
     */
    protected void printResponse(HttpServletResponse response) throws Exception {
        this.printer.printValue("Status", response.getStatus());
        this.printer.printValue("Headers", getResponseHeaders(response));
        this.printer.printValue("Content type", response.getContentType());
        // this.printer.printValue("Body", response.getContentAsString());
        // this.printer.printValue("Forwarded URL", response.getForwardedUrl());
        // this.printer.printValue("Redirected URL", response.getRedirectedUrl());
        // printCookies(response.getCookies());
    }

    /**
     * Print the supplied cookies in a human-readable form, assuming the {@link Cookie} implementation does not provide its own {@code toString()}.
     * 
     * @since 4.2
     */
    private void printCookies(Cookie[] cookies) {
        String[] cookieStrings = { "-" };
        if (cookies != null) {
            cookieStrings = new String[cookies.length];
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                cookieStrings[i] = new ToStringCreator(cookie).append("name", cookie.getName()).append("value", cookie.getValue()).append("comment", cookie.getComment())
                        .append("domain", cookie.getDomain()).append("maxAge", cookie.getMaxAge()).append("path", cookie.getPath()).append("secure", cookie.getSecure())
                        .append("version", cookie.getVersion()).append("httpOnly", cookie.isHttpOnly()).toString();
            }
        }
        this.printer.printValue("Cookies", cookieStrings);
    }

    protected final HttpHeaders getResponseHeaders(HttpServletResponse response) {
        HttpHeaders headers = new HttpHeaders();
        for (String name : response.getHeaderNames()) {
            headers.put(name, new ArrayList<String>(response.getHeaders(name)));
        }
        return headers;
    }

    /**
     * A contract for how to actually write result information.
     */
    protected interface ResultValuePrinter extends Closeable {

        void printHeading(String heading);

        void printValue(String label, Object value);
    }

}
